﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using com.bodurov.SilverlightControls.XmlCodeEditor.Enums;
using com.bodurov.SilverlightControls.XmlCodeEditor.Extensions;
using com.bodurov.SilverlightControls.XmlCodeEditor.Utils;

namespace com.bodurov.SilverlightControls.XmlCodeEditor
{
    public class XmlCodeEditorBox : RichTextBox
    {
        
        
        private readonly Queue<KeyEventArgs> _keyPressEvents = new Queue<KeyEventArgs>();
        private readonly RichTextBoxContentManager _manager;

        private const int PlatformKeyCode_Back = 8;//Key.Delete
        private const int PlatformKeyCode_Delete = 46;//Key.Back

        private const int PlatformKeyCode_At = 50;
        private const int PlatformKeyCode_Percent = 53;
        private const int PlatformKeyCode_Quote = 222;
        private const int PlatformKeyCode_OpenAngleBracket = 188;
        private const int PlatformKeyCode_CloseAngleBracket = 190;
        private const int PlatformKeyCode_EqualSign = 187;
        private const int PlatformKeyCode_Colon = 186;
        private const int PlatformKeyCode_Space = 32;
        private const int PlatformKeyCode_Tab = 9;

        private ScrollViewer _scrollViewer;

        private readonly HashSet<int> _changingKeys = 
            new HashSet<int> {  PlatformKeyCode_At, PlatformKeyCode_Percent, PlatformKeyCode_Quote, 
                                PlatformKeyCode_OpenAngleBracket, PlatformKeyCode_CloseAngleBracket,
                                PlatformKeyCode_EqualSign, PlatformKeyCode_Colon, PlatformKeyCode_Space, 
                                PlatformKeyCode_Tab, PlatformKeyCode_Back, PlatformKeyCode_Delete };

        private Popup _menuPopup;
        private FindPopup _findTextPopup;
        public XmlCodeEditorBox()
        {
            this._manager = new RichTextBoxContentManager(this);
            this.AcceptsReturn = true;
            this.TabNavigation = KeyboardNavigationMode.Local;
            this.HorizontalScrollBarVisibility = this.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
            this.Tab = "    ";
            this.ContentChanged += XmlCodeEditorBox_ContentChanged;
            this.LayoutUpdated += XmlCodeEditorBox_LayoutUpdated;

            Application.Current.Host.Content.Resized += Content_Resized;
        }

        private void Content_Resized(object sender, EventArgs e)
        {
            HideFindTextBox();
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            ClearSearchIndex();
            base.OnMouseLeftButtonDown(e);
            this.HideMenu();
        }

        protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseRightButtonDown(e);

            ShowPopup(e.GetPosition(this));

            if (_menuPopup.IsOpen) e.Handled = true;
        }

        private void ShowFindTextBox()
        {
            if(_findTextPopup == null)
            {
                _findTextPopup = CreateFindTextPopup();
            }
            var textBox = _findTextPopup.TextBox;
            var selectedText = this.Selection.Text;
            if(!String.IsNullOrEmpty(selectedText))
            {
                _findTextPopup.TextBox.Text = selectedText.Trim();
            }
            _findTextPopup.Popup.VerticalOffset = 0;
            _findTextPopup.Popup.HorizontalOffset = Application.Current.Host.Content.ActualWidth - ((FrameworkElement)textBox.Parent).Width - 22;
            _findTextPopup.Popup.IsOpen = true;
            textBox.Focus();
        }
        private void HideFindTextBox()
        {
            if (_findTextPopup == null)
            {
                _findTextPopup = CreateFindTextPopup();
            }
            _findTextPopup.Popup.IsOpen = false;
        }

        private void ShowPopup(Point currentMousePosition)
        {
            if (_menuPopup == null)
            {
                _menuPopup = CreateContextMenu();
            }
            _menuPopup.HorizontalOffset = currentMousePosition.X;
            _menuPopup.VerticalOffset = currentMousePosition.Y;
            _menuPopup.IsOpen = true;
        }
        private void HideMenu()
        {
            if (_menuPopup != null)
            {
                _menuPopup.IsOpen = false;
                _menuPopup.InvalidateArrange();
            }
        }
        
        private Popup CreateContextMenu()
        {
            var popup = new Popup();
            var popupGrid = new Grid();
            var popupCanvas = new Canvas();

            popup.Child = popupGrid;
            popupCanvas.MouseLeftButtonDown += (sender, e) => HideMenu();
            popupCanvas.MouseRightButtonDown += (sender, e) => { e.Handled = true; HideMenu(); };
            popupCanvas.Background = new SolidColorBrush(Colors.Transparent);
            popupGrid.Children.Add(popupCanvas);
            popupCanvas.Children.Add(CreateContextMenuItems());
            
            popupGrid.Width = Application.Current.Host.Content.ActualWidth;
            popupGrid.Height = Application.Current.Host.Content.ActualHeight;
            popupCanvas.Width = popupGrid.Width;
            popupCanvas.Height = popupGrid.Height;

            return popup;
        }

        private FindPopup CreateFindTextPopup()
        {
            var result = new FindPopup();
            var popup = new Popup();
            result.Popup = popup;

            var popupGrid = new Grid();
            var popupCanvas = new Canvas();

            popup.Child = popupGrid;
            popupCanvas.Background = new SolidColorBrush(Colors.Transparent);
            var panel = CreateFindInputs(result);
            popupCanvas.Height = ((TextBox) popup.Tag).Height;
            popupGrid.Children.Add(popupCanvas);
            popupCanvas.Children.Add(panel);

            popupGrid.Width = Application.Current.Host.Content.ActualWidth;
            popupGrid.Height = popupGrid.Height;
            popupCanvas.Width = popupGrid.Width;

            return result;
        }

        internal class FindPopup
        {
            internal Popup Popup;
            internal TextBox TextBox;
            internal Button Button;
        }

        private FrameworkElement CreateContextMenuItems()
        {

            var lstContextMenu = new ListBox();

            var txb = new TextBlock { Text = "Find  Ctrl+F", Width = 100, Tag = "Find" };
            txb.MouseLeftButtonDown += Menu_Click;
            lstContextMenu.Items.Add(txb);

            txb = new TextBlock { Text = "Cut  Ctrl+X", Width = 100, Tag = "Cut" };
            txb.MouseLeftButtonDown += Menu_Click;
            lstContextMenu.Items.Add(txb);

            txb = new TextBlock { Text = "Copy  Ctrl+C", Width = 100, Tag = "Copy" };
            txb.MouseLeftButtonDown += Menu_Click;
            lstContextMenu.Items.Add(txb);

            txb = new TextBlock { Text = "Paste  Ctrl+P", Width = 100, Tag = "Paste"};
            txb.MouseLeftButtonDown += Menu_Click;
            lstContextMenu.Items.Add(txb);

            

            var rootGrid = new Grid()
            {
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Top,
                //Margin = new Thickness(currentMousePosition.X, currentMousePosition.Y, 0, 0)
            };
            rootGrid.Children.Add(lstContextMenu);

            return rootGrid;
        }

        private FrameworkElement CreateFindInputs(FindPopup popup)
        {
            var panel = new StackPanel { Orientation = Orientation.Horizontal, Width = 222, Height = 22 };
            var textBox = new TextBox { Width = 120 };
            textBox.KeyDown += (o, e) =>
                                   {
                                       if (e.Key == Key.Escape) HideFindTextBox();
                                       else if (e.Key == Key.Enter)
                                       {
                                           SearchForNextText(textBox.Text);
                                           e.Handled = true;
                                       }
                                   };

            var findButton = new Button { Content = "Find Next", Tag = textBox, Width = 80 };
            findButton.Click += Find_Button_Click;
            popup.Button = findButton;

            var closeButton = new Button { Content = "X", Tag = textBox, Width = 22 };
            closeButton.Click += (o, e) => HideFindTextBox();
            panel.Children.Add(textBox);
            panel.Children.Add(findButton);
            panel.Children.Add(closeButton);
            popup.Popup.Tag = textBox;
            popup.TextBox = textBox;
            return panel;
        }

        private void Find_Button_Click(object sender, RoutedEventArgs e)
        {
            var tb = (TextBox) ((Button) sender).Tag;
            var text = tb.Text;

            SearchForNextText(text);
        }

        private int _lastSearchIndex;
        private TextPointer _lastSearchIndexPointer;
        private string _lastSearchString;


        private void SearchForNextText(string text)
        {
            if (!CanFindSubstring(text)) MessageBox.Show("Cannot find '"+text+"'");
        }


        private bool CanFindSubstring(string text)
        {
            if (String.IsNullOrEmpty(text)) return true;
            text = text.Trim();
            if (text == "") return false;
            if (this.Blocks.Count == 0) return false;
            var content = this.Text;
            if (String.IsNullOrEmpty(text)) return false;

            _findTextPopup.Button.IsEnabled = false;

            if (_lastSearchString == null || !String.Equals(_lastSearchString, text, StringComparison.OrdinalIgnoreCase))
            {
                ClearSearchIndex();
            }
            _lastSearchString = text;
            if ((_lastSearchIndex + 1) >= content.Length)
            {
                ClearSearchIndex();
            }
            var index = content.IndexOf(text, _lastSearchIndex + 1, StringComparison.OrdinalIgnoreCase);
            if (index >= 0)
            {
                TextPointer from, to;
                // we are looking for the second or higher match
                if (_lastSearchIndex > 0 && _lastSearchIndexPointer != null && (index - _lastSearchIndex) > 0)
                {
                    var contentStart = _lastSearchIndexPointer;
                    from = FindUtil.GetPointerAtCharacterOffset(contentStart, (index - _lastSearchIndex));
                    to = FindUtil.GetPointerAtCharacterOffset(from, text.Length);
                }
                else
                {
                    var contentStart = this.Blocks[0].ContentStart;
                    from = FindUtil.GetPointerAtCharacterOffset(contentStart, index);
                    to = FindUtil.GetPointerAtCharacterOffset(from, text.Length);
                }
                SetSearchIndex(index, from);
                
                if (from != null && to != null)
                {
                    this.Focus();
                    this.Selection.Select(from, to);
                    _findTextPopup.Button.IsEnabled = true;
                    return true;
                }
            }
            else if(content.IndexOf(text, 0, StringComparison.OrdinalIgnoreCase) >= 0)
            {
                ClearSearchIndex();
                MessageBox.Show("Reached the end of the document");
                _findTextPopup.Button.IsEnabled = true;
                return true;
            }
            _findTextPopup.Button.IsEnabled = true;
            return false;
        }

        private void ClearSearchIndex()
        {
            _lastSearchIndex = 0;
            _lastSearchIndexPointer = null;
        }
        private void SetSearchIndex(int index, TextPointer pointer)
        {
            _lastSearchIndex = index;
            _lastSearchIndexPointer = pointer;
        }

        private void Menu_Click(object sender, MouseButtonEventArgs e)
        {
            HideMenu();
            var element = ((TextBlock) sender);
            if (element.Tag.ToString() == "Copy")
            {
                this.Copy();
            }
            else if (element.Tag.ToString() == "Paste")
            {
                this.Paste();
            }
            else if (element.Tag.ToString() == "Cut")
            {
                this.Cut();
            }
            else if (element.Tag.ToString() == "Find")
            {
                this.Find();
            }
            
        }


        public string Tab { get; set; }

        public void ScrollToTop()
        {
            this.ScrollViewer.ScrollToVerticalOffset(0);
            this.ScrollViewer.ScrollToHorizontalOffset(0);
        }

        public ScrollViewer ScrollViewer
        {
            get
            {
                if (this._scrollViewer == null)
                {
                    this._scrollViewer = GetTemplateChild("ContentElement") as ScrollViewer;
                }
                return this._scrollViewer;
            }
        }

        private void Copy()
        {
            var copiedText = this.Selection.Text;
            Clipboard.SetText(copiedText);
            this.Focus();
        }

        private void Cut()
        {
            var copiedText = this.Selection.Text;
            Clipboard.SetText(copiedText);
            this._manager.PerformPaste("");
            this.Focus();
        }

        private void Paste()
        {
            var cbText = Clipboard.GetText();
            this._manager.PerformPaste(cbText);

        }

        private void Undo()
        {
            this._manager.Undo();
        }

        private void Redo()
        {
            this._manager.Redo();
        }
        
        private void Find()
        {
            ShowFindTextBox();
        }

        protected override void OnKeyDown(KeyEventArgs e)
        {
            if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
            {
                if(e.Key == Key.A)
                {
                    this.SelectAll();
                }
                else if (e.Key == Key.C)
                {
                    this.Copy();
                }
                else if (e.Key == Key.X)
                {
                    this.Cut();
                }
                else if (e.Key == Key.V)
                {
                    this.Paste();
                }
                else if (e.Key == Key.Z)
                {
                    this.Undo();
                }
                else if (e.Key == Key.Y)
                {
                    this.Redo();
                }
                else if (e.Key == Key.F)
                {
                    this.Find();
                }
                else if (e.Key == Key.H)
                {
                    this.Find();
                }
                e.Handled = true;
                return;
            }
            if(e.Key == Key.Escape)
            {
                HideMenu();
                HideFindTextBox();
            }
            if (e.Key == Key.Enter && _lastSearchIndex > 0)
            {
                _findTextPopup.TextBox.Focus();
                Find_Button_Click(_findTextPopup.Button, new RoutedEventArgs());
                e.Handled = true;
            }
            base.OnKeyDown(e);

            if (e.Key == Key.Tab)
            {
                e.Handled = true;
                this._manager.HandleTabClick(Tab, (Keyboard.Modifiers & ModifierKeys.Shift) > 0);
            }
            //Dbg.Trace(">>>"+e.Key+"|"+e.PlatformKeyCode);
            _keyPressEvents.Enqueue(e);
        }


        private void XmlCodeEditorBox_ContentChanged(object sender, ContentChangedEventArgs e)
        {
            ClearSearchIndex();
            while (_keyPressEvents.Count > 0)
            {
                var k = _keyPressEvents.Dequeue();
                if(k.Key == Key.Enter)
                {
                    var currentParagraph = this.Selection.End.GetParagraph();
                    var previousParagraph = currentParagraph.GetPrevious();
                    this._manager.RegenerateParagraph(k.PlatformKeyCode, previousParagraph);
                    this._manager.RegenerateParagraph(k.PlatformKeyCode, currentParagraph, previousParagraph.GetWhiteSpaceOnTheLeft());
                }
                else if (_changingKeys.Contains(k.PlatformKeyCode))
                {
                    this._manager.RegenerateParagraph(k.PlatformKeyCode, this.Selection.End.GetParagraph());
                }
                
            }
        }

        public bool HasNoTokenOrContentIsChange(Run run)
        {
            var token = _manager.GetTokenByRun(run);
            return (token == null) ? true : token.Content != run.Text;
        }

        public string Text
        {
            get
            {
                return GetPlainText();
            }
            set
            {
                if (String.IsNullOrEmpty(value))
                {
                    this._manager.ClearCache();
                    this.Blocks.Clear();
                }
                else
                {
                    this.SetTextAndPositionCursor(value, -1);
                    // scroll up
                    if(this.Blocks.Count > 0)
                    {
                        this.Selection.Select(this.Blocks[0].ContentStart, this.Blocks[0].ContentStart);
                    }
                }
            }
        }

        
        private string GetPlainText()
        {
            var sb = new StringBuilder();

            var isFirst = true;
            foreach (Paragraph paragraph in this.Blocks)
            {
                if (isFirst) isFirst = false;
                else sb.AppendLine();
                sb.Append(paragraph.Inlines.GetContent());
            }

            return sb.ToString();
        }

        public void SetTextAndPositionCursor(string text, int cursorOffset)
        {
            this._manager.RegenerateAll(text, cursorOffset);
        }

        private void XmlCodeEditorBox_LayoutUpdated(object sender, EventArgs e)
        {
            this._manager.OnLayoutUpdated();
        }

    }
}
